﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;

using FirstFloor.Documents.Services.Storage;
using Ionic.Zip;

namespace FirstFloor.Documents.Services.RequestHandlers
{
    /// <summary>
    /// Returns the requested package part from the XPS document.
    /// </summary>
    public class GetDocumentPartHandler
        : IRequestHandler
    {
        private static bool IsModified(HttpContext context, DateTime lastModified)
        {
            string value = context.Request.Headers["If-Modified-Since"];
            if (value != null) {
                DateTime ifModifiedSince;
                if (DateTime.TryParse(value, out ifModifiedSince)) {
                    // ensures milliseconds and ticks are not taken into account (needed when determining HTTP if not modified since)
                    lastModified = new DateTime(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second);

                    return ifModifiedSince != lastModified;
                }
            }
            return true;
        }

        private static void WriteToResponse(HttpContext context, DateTime lastModified, string contentType, Stream stream)
        {
            // ensures lastmodified does not exceed now
            if (lastModified > DateTime.Now) {
                lastModified = DateTime.Now;
            }

            context.Response.ContentType = contentType;
            context.Response.Cache.SetLastModified(lastModified);
            context.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(1));      // expires in 24 hours

            // compress output (omit for images and zip content)
            if (contentType != "application/zip" && !contentType.StartsWith("image/")) {
                var acceptEncoding = context.Request.Headers["Accept-Encoding"];
                if (acceptEncoding != null) {
                    if (acceptEncoding.Contains("gzip")) {
                        context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
                        context.Response.AppendHeader("Content-Encoding", "gzip");

                    }
                    else if (acceptEncoding.Contains("deflate")) {
                        context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress);
                        context.Response.AppendHeader("Content-Encoding", "deflate");
                    }
                }
            }

            stream.CopyTo(context.Response.OutputStream);
        }

        /// <summary>
        /// Handles the request.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="store">The store.</param>
        public void HandleRequest(HttpContext context, IDocumentStore store)
        {
            var query = HttpUtility.UrlDecode(context.Request.QueryString.ToString());
            var index = query.IndexOf('/');
            if (index == -1) {
                throw new ArgumentException("Invalid request");
            }
            var name = query.Substring(0, index);
            var partName = query.Substring(index);

            if (!store.ContainsDocument(name)) {
                // document not found, return 404
                context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                return;
            }

            var lastModified = store.GetDocumentLastModified(name);

            if (!IsModified(context, lastModified)) {
                // document not modified, return 304 not modified
                context.Response.StatusCode = (int)HttpStatusCode.NotModified;
                context.Response.StatusDescription = "Not Modified";

                return;
            }


            using (var document = store.OpenDocument(name)) {
                var part = store.GetDocumentPart(document, partName);

                if (part == null) {
                    // part not found, return 404
                    context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                    return;
                }

                // if request is a POST and part is a page, collect all required font and image resources and return all in one zip
                var sendSinglePart = !(context.Request.HttpMethod == "POST" && part.ContentType == "application/vnd.ms-package.xps-fixedpage+xml");

                if (!sendSinglePart) {
                    var parts = GetPageAndRequiredResources(document, store, part).ToArray();

                    // read the part uris that are already on the client from the request body
                    var cachedPartNames = GetCachedPartNames(context);

                    // exclude parts already available on the client
                    var partsToSend = parts.Where(p => !cachedPartNames.ContainsKey(p.PartName)).ToArray();

                    if (partsToSend.Length == 1) {
                        // just 1 part to send, do not zip, send part as-is
                        sendSinglePart = true;
                    }
                    else {
                        try {
                            // create zip to package N parts
                            using (var zipFile = new ZipFile()) {
                                // add parts
                                foreach (var p in partsToSend) {
                                    zipFile.AddEntry(p.PartName, p.Stream);
                                }

                                // write zip file
                                using (var zipStream = new MemoryStream()) {
                                    zipFile.Save(zipStream);
                                    zipStream.Seek(0, SeekOrigin.Begin);

                                    WriteToResponse(context, lastModified, "application/zip", zipStream);
                                }
                            }
                        }
                        finally {
                            // make sure all part streams are properly disposed
                            foreach (var p in parts) {
                                p.Stream.Dispose();
                            }
                        }
                    }
                }


                if (sendSinglePart) {   
                    // write part as-is
                    using (part.Stream) {
                        WriteToResponse(context, lastModified, part.ContentType, part.Stream);
                    }
                }
            }
        }

        private static Dictionary<string, object> GetCachedPartNames(HttpContext context)
        {
            var result = new Dictionary<string, object>();

            if (context.Request.HttpMethod == "POST") {
                // read part uri's per line
                using (var reader = new StreamReader(context.Request.InputStream)) {
                    string partUri = reader.ReadLine();
                    while (partUri != null) {
                        result.Add(partUri, null);

                        partUri = reader.ReadLine();
                    }
                }
            }

            return result;
        }

        private static IEnumerable<DocumentPartInfo> GetPageAndRequiredResources(Stream document, IDocumentStore store, DocumentPartInfo pagePart)
        {
            yield return pagePart;      // page itself

            if (pagePart.RequiredPartNames != null) {
                foreach (var name in pagePart.RequiredPartNames) {
                    var part = store.GetDocumentPart(document, name);

                    // only take fonts and images into account
                    if (part.ContentType != null && (part.ContentType.StartsWith("image/") || part.ContentType == "application/vnd.ms-opentype" || part.ContentType == "application/vnd.ms-package.obfuscated-opentype")) {
                        yield return part;
                    }
                }
            }
        }
    }
}
